import Axios, { AxiosRequestConfig, AxiosPromise } from 'axios'
import { calculateMd5 } from './Utils'
import {
  uploadConfig,
  getCategoryFrom,
  getTransformConfig,
  ModelTransformStatus,
  SubType,
  setUploadConfig,
} from './uploadServiceConfig'
import { downloadFileByGuid } from './downloadChunks'

let supportedFileTypes = ['rvt', 'fbx', 'dwg']

export const applyDataToUrl = (url: string, data: Record<string, string>) => {
  // url = query/user/:userid   data { userid: 8283131, ID:123 }
  // result url = query/user/8283131
  // data {ID:123}
  // 剩余的data参数 会根据请求的不同 放到 请求的url里面或者请求的body里面
  if (typeof data !== 'object') {
    return url
  }
  if (Array.isArray(data)) {
    return url
  }
  for (const key in data) {
    if (url.indexOf(':' + key) > -1) {
      url = url.replace(':' + key, data[key])
      delete data[key]
    }
  }
  return url
}

enum UploadStatusServer {
  FILE_NOT_EXIST,
  FILE_EXIST_IN_SERVER,
}

function getFileExt(filename: string): string {
  const lastIndexOfDot = filename.lastIndexOf('.')
  return lastIndexOfDot === -1 ? '' : filename.slice(lastIndexOfDot + 1)
}

export interface FilePieceUploadStatus {
  size: number
  url: string
  headers: any
  chunkId: number
}

interface FileUploadStatusResponse {
  status: UploadStatusServer
  list: FilePieceUploadStatus[]
}

interface ChunkPromiseFn {
  (): AxiosPromise<any>
  isUploading: boolean
  chunkId: number
}

interface TransformInfo {
  status: ModelTransformStatus
  statusInfo?: {
    queue?: number // 排队
    progress?: number // 转换进度
  }
}

export interface UploadItem {
  status: UploadStatus
  file: UploadFileType
  guid: string
  md5: string
  id: number | string
  progress?: number // 0-100 上传进度
  transformInfo?: TransformInfo // 转换进度
  chunksProgressArr: number[]
  totalChunksNum?: number
  options?: AxiosRequestConfig
  callback?: (data: any) => void
  progressNotifyCallback?: (progressNum: number) => void
  chunkUploadPromiseQueue: ChunkPromiseFn[]
  chunkUploadingPromiseQueue: ChunkPromiseFn[]
}

interface CalcMD5Item {
  id: string | number
  calcPromise: Promise<string>
}

export enum UploadStatus {
  WAITING,
  PENDING,
  RESOLVED,
  REJECTED,
  PAUSED,
}

type UploadFileType = File & {
  customExt?: string
  customName?: string
}

// const { validateMD5, mergeFileChunks } = API

// 提示函数 对接上层的提示函数
const message = {
  warn: console.error,
  warning: console.error,
  success: console.log,
}

class FileUploader {
  public resolvedQueue: UploadItem[] = [] // 上传成功队列
  private queue: UploadItem[] = []
  private allQueue: UploadItem[] = [] // 所有上传队列
  private uploadQueue: UploadItem[] = []
  private maxUploadNum = 3
  private maxUploadChunkNum = 3
  private rejectedQueue: UploadItem[] = [] // 上传失败队列
  private pausedQueue: UploadItem[] = [] // 暂停队列
  private chunkSize = 5 * 1024 * 1024 //分片大小 如果不需要分片 可以设置足够大的分块 99999x1024x1024
  private md5CalcQueue: CalcMD5Item[] = []
  private uploadId = 0
  private waitingForCalcMd5Queue: UploadItem[] = []
  private _globalToken = ''
  private _globalUploadDirId = ''
  private _globalUploadAppId = ''

  constructor(chunkSize = 5 * 1024 * 1024) {
    this.chunkSize = chunkSize
  }

  get globalToken(): string {
    return this._globalToken
  }

  set globalToken(token: string) {
    this._globalToken = token
  }

  get globalUploadDirId(): string {
    return this._globalUploadDirId
  }

  set globalUploadDirId(dirId: string) {
    this._globalUploadDirId = dirId
  }

  get globalUploadAppId(): string {
    return this._globalUploadAppId
  }

  set globalUploadAppId(appId: string) {
    this._globalUploadAppId = appId
  }

  addToUpload(
    uploadFile: UploadFileType,
    options?: AxiosRequestConfig,
    progressNotifyCallback?: (progressNum: number) => void,
    uploadSuccallback?: (data: any) => void,
    warningCallback?: (data: any) => void
  ): void {
    if (!uploadFile) return
    const ext = getFileExt(uploadFile.name).toLowerCase()
    if (!supportedFileTypes.includes(ext)) {
      message.warn('不支持的文件类型')
      warningCallback && warningCallback(supportedFileTypes)
      return
    }
    this.add(uploadFile, options, progressNotifyCallback, uploadSuccallback)
    this.startUpload()
  }

  requestTransStatus(guid: string): void {
    console.log(' 请求转换状态 ', guid)
  }

  public async checkTransformStatusByGuidAry(guidAry: string[]) {
    const result = await Axios.request({
      url: uploadConfig.baseUrl + uploadConfig.checkTransformStatus,
      method: 'post',
      headers: {
        'access-token': this.globalToken,
      },
      data: guidAry,
    })
    return result
  }

  public async checkTransformStatus() {
    // 只检测 上传完成 并且没有转换成功的
    const guidAry = this.resolvedQueue
      .map(item => {
        if (!item.transformInfo) {
          return item.guid
        }
        if (item.transformInfo) {
          if (
            +item.transformInfo.status !== ModelTransformStatus.SUCCESS &&
            +item.transformInfo.status !== ModelTransformStatus.FAIL &&
            +item.transformInfo.status !== ModelTransformStatus.CANCEL
          ) {
            return item.guid
          }
        }

        return undefined
      })
      .filter(guid => guid !== undefined) as string[]
    if (guidAry.length > 0) {
      const result = await this.checkTransformStatusByGuidAry(guidAry)
      if (result.data && result.data.code === 200) {
        const data = result.data.data
        console.log(' red --- ')
        this.resolvedQueue.forEach(item => {
          if (data[item.guid]) {
            item.transformInfo = data[item.guid]
          }
        })
      }
      // this.resolvedQueue
    }
  }

  private add(
    uploadFile: UploadFileType,
    options: AxiosRequestConfig = {},
    progressNotifyCallback?: (progressNum: number) => void,
    uploadSuccallback?: (data: any) => void,
  ): void {
    const uploadItem: UploadItem = {
      status: UploadStatus.WAITING,
      id: this.uploadId++,
      md5: '',
      guid: '',
      file: uploadFile,
      options,
      chunksProgressArr: [],
      callback: uploadSuccallback,
      chunkUploadPromiseQueue: [],
      chunkUploadingPromiseQueue: [],
      progressNotifyCallback,
    }
    this.queue.push(uploadItem)
    this.allQueue.push(uploadItem)
    this.waitingForCalcMd5Queue.push(uploadItem)
  }

  private async startUpload(): Promise<void> {
    this.prepareQueue()
    this.updateCalcQ()

    this.uploadQueue.forEach((item: UploadItem) => {
      if (item.status === UploadStatus.WAITING) {
        item.status = UploadStatus.PENDING
        this.uploadSingleFile(item)
      }
    })
  }

  private async uploadSingleFile(item: UploadItem): Promise<void> {
    try {
      const md5 = await this.getCalcMd5Promise(item)
      if (item.status === UploadStatus.PAUSED) return

      const res = await this.validateMD5Info(md5, item)

      const data: FileUploadStatusResponse = res.data
      if (data.status === UploadStatusServer.FILE_EXIST_IN_SERVER) {
        item.status = UploadStatus.RESOLVED
        item.progress = 100
        await this.mergeFileChunk(item)
        this.uploadFinal(item)
      } else {
        const { list } = data
        const listWithChunkId: FilePieceUploadStatus[] = list.map(
          (item, index) => {
            item.chunkId = index
            return item
          },
        )

        this.prepareChunkUploadQueue(
          this.getUnfinishedChunkArr(listWithChunkId, item),
          item,
        )
        this.updateChunksUploadingQueue(item)
        this.uploadingChunksInParallel(item)
      }
    } catch (error: any) {
      message.warn(error.message)
      item.status = UploadStatus.REJECTED
      this.uploadFinal(item)
    }
  }

  private uploadingChunksInParallel(item: UploadItem): void {
    const { chunkUploadingPromiseQueue } = item
    if (chunkUploadingPromiseQueue.length === 0) {
      if (item.status === UploadStatus.PENDING) {
        item.status = UploadStatus.RESOLVED
        item.progress = 100
        this.mergeFileChunk(item)
        this.uploadFinal(item)
      }
      return
    }

    chunkUploadingPromiseQueue.forEach((chunkPromiseFn: ChunkPromiseFn) => {
      if (!chunkPromiseFn.isUploading) {
        chunkPromiseFn.isUploading = true
        chunkPromiseFn()
          .then(() => {
            this.removeUploadedChunk(item, chunkPromiseFn.chunkId)
            this.updateChunksUploadingQueue(item)
            this.uploadNextChunk(item)
          })
          .catch(err => {
            message.warn('上传失败: ' + err.message)
            item.status = UploadStatus.REJECTED
            this.queue.push(item)
            this.stopUpload(item)
            this.uploadFinal(item)
          })
      }
    })
  }

  private removeUploadedChunk(item: UploadItem, chunkId: number): void {
    const index = item.chunkUploadingPromiseQueue.findIndex(
      chunk => chunk.chunkId === chunkId,
    )
    if (index !== -1) item.chunkUploadingPromiseQueue.splice(index, 1)
  }

  private stopUpload(item: UploadItem): void {
    item.chunkUploadingPromiseQueue = []
    item.chunkUploadPromiseQueue = []
  }

  removeFromWaitingQueue(item: UploadItem) {
    const { queue } = this
    const { id } = item
    let deleteItemIndex = -1
    queue.map((item: UploadItem, index: number) => {
      if (item.id === id) {
        deleteItemIndex = index
      }
    })
    if (deleteItemIndex > -1) {
      queue.splice(deleteItemIndex, 1)
    }
  }

  public pauseUpload(item: UploadItem): void {
    if (item.status === UploadStatus.PENDING) {
      this.stopUpload(item)
    }
    if (item.status === UploadStatus.WAITING) {
      // 1.delete from waitingQ
      this.removeFromWaitingQueue(item)
    }
    // change UploadStatus
    item.status = UploadStatus.PAUSED
    // end upload update Queue and upload next item
    this.uploadFinal(item)
  }

  private removeFromPausedQueue(item: UploadItem) {
    // TODO: 重构 removeFromPausedQueue removeFromWaitingQueue uploadFinal 三个实现一模一样只是对象不一样
    const { pausedQueue } = this
    const { id } = item
    let deleteItemIndex = -1
    pausedQueue.map((item: UploadItem, index: number) => {
      if (item.id === id) {
        deleteItemIndex = index
      }
    })
    if (deleteItemIndex > -1) {
      pausedQueue.splice(deleteItemIndex, 1)
    }
  }
  private removeFromRejectedQueue(item: UploadItem) {
    // TODO: 重构 removeFromPausedQueue removeFromWaitingQueue uploadFinal 三个实现一模一样只是对象不一样
    const { rejectedQueue } = this
    const { id } = item
    let deleteItemIndex = -1
    rejectedQueue.map((item: UploadItem, index: number) => {
      if (item.id === id) {
        deleteItemIndex = index
      }
    })
    if (deleteItemIndex > -1) {
      rejectedQueue.splice(deleteItemIndex, 1)
    }
  }

  public resumeUpload(item: UploadItem) {
    const { queue } = this
    // pausedQueue => queue
    this.removeFromPausedQueue(item)
    this.removeFromRejectedQueue(item)
    item.status = UploadStatus.WAITING
    queue.push(item)
    // start upload
    this.startUpload()
  }

  private uploadNextChunk(item: UploadItem): void {
    this.uploadingChunksInParallel(item)
  }

  private updateChunksUploadingQueue(item: UploadItem): void {
    while (
      item.chunkUploadingPromiseQueue.length < this.maxUploadChunkNum &&
      item.chunkUploadPromiseQueue.length > 0
    ) {
      const nextChunk = item.chunkUploadPromiseQueue.shift()
      if (nextChunk) item.chunkUploadingPromiseQueue.push(nextChunk)
    }
  }

  private async mergeFileChunk(item: UploadItem): Promise<void> {
    const { md5, file } = item
    const { name, customExt, customName } = file
    const ext = customExt || getFileExt(item.file.name).toLowerCase()
    const convertParams = getTransformConfig(ext as SubType)
    const category = getCategoryFrom(ext as SubType)
    const type = ext.toUpperCase()
    try {
      const totalChunks = Math.max(
        Math.ceil(item.file.size / this.chunkSize),
        1,
      )
      const url = uploadConfig.baseUrl + uploadConfig.mergeModelFileChunks
      const result = await Axios.request({
        url,
        method: 'post',
        data: Object.assign(
          {},
          {
            fileMd5: item.md5,
            separateNum: totalChunks,
            fileName: customName || name,
            fileSize: item.file.size,
            type, // NOTE: 规定了大写
            mimeType: item.file.type,
            metaType: category,
            // ...(item.options || {}),
            appId: this.globalUploadAppId,
            dirId: this.globalUploadDirId,
          },
          {
            ...item.options?.data,
          },
          { ...convertParams },
        ),
        headers: Object.assign(
          {},
          {
            'access-token': this.globalToken,
          },
          item.options?.headers,
        ),
      })
      console.log(' --- merge success --- ', result)
      if (result.status === 200 && result.data && result.data.code === 200) {
        //
        console.log(' --- merge success --- ', result.data.data)
        item.guid = result.data.data
        item.callback?.(result.data.data)
      }
    } catch (err) {
      message.warning('因网络错误，创建模型失败！请稍后再试')
      console.error(err)
    }
  }

  private uploadFinal(item: UploadItem): void {
    // Remove from uploadQueue
    const index = this.uploadQueue.findIndex(i => i.id === item.id)
    if (index !== -1) this.uploadQueue.splice(index, 1)

    // Move to appropriate queue
    switch (item.status) {
      case UploadStatus.RESOLVED:
        this.resolvedQueue.push(item)

        // Clean up item to release memory
        this.cleanupUploadItem(item)
        break
      case UploadStatus.REJECTED:
        this.rejectedQueue.push(item)
        break
      case UploadStatus.PAUSED:
        this.pausedQueue.push(item)
        break
    }

    // Start next upload
    this.startUpload()
  }

  private async getCalcMd5Promise(item: UploadItem): Promise<string> {
    if (item.md5) return item.md5

    const existingCalc = this.md5CalcQueue.find(calc => calc.id === item.id)
    if (existingCalc) return existingCalc.calcPromise

    const calcPromise = this.calcMd5(item)
    this.md5CalcQueue.push({ id: item.id, calcPromise })
    return calcPromise
  }

  private async calcMd5(item: UploadItem): Promise<string> {
    try {
      const md5 = await calculateMd5(item.file)
      item.md5 = md5
      this.calcMd5Final(item)
      return md5
    } catch {
      throw new Error('MD5 calculation failed')
    }
  }

  private calcMd5Final(item: UploadItem): void {
    this.md5CalcQueue = this.md5CalcQueue.filter(calc => calc.id !== item.id)
    this.updateCalcQ()
  }

  private updateCalcQ(): void {
    while (
      this.md5CalcQueue.length < 3 &&
      this.waitingForCalcMd5Queue.length > 0
    ) {
      const nextItem = this.waitingForCalcMd5Queue.shift()
      if (nextItem) {
        const calcPromise = this.calcMd5(nextItem)
        this.md5CalcQueue.push({ id: nextItem.id, calcPromise })
      }
    }
  }

  private prepareQueue(): void {
    while (
      this.uploadQueue.length < this.maxUploadNum &&
      this.queue.length > 0
    ) {
      const nextItem = this.queue.shift()
      if (nextItem) this.uploadQueue.push(nextItem)
    }
  }

  private async validateMD5Info(md5: string, item: UploadItem): Promise<any> {
    const totalChunks = Math.max(Math.ceil(item.file.size / this.chunkSize), 1)
    const url = uploadConfig.baseUrl + uploadConfig.uploadModelUrl
    // url = applyDataToUrl(url, item.options?.params)
    return Axios.request({
      url: url,
      method: 'post',
      headers: Object.assign(
        {},
        {
          'access-token': this.globalToken,
        },
        item.options?.headers, // NOTE: 优先级最高
      ),
      data: Object.assign(
        {},
        {
          fileMd5: md5,
          separateNum: totalChunks,
          filename: item.file.name,
          fileSize: item.file.size,
        },
        {
          ...item.options?.data, // NOTE: 额外参数
        },
      ),
      // ...(item.options || {}),
      // params: (item.options && item.options.params) || {},
    })
      .then(res => {
        console.log(' res --- ', res)
        if (res.status === 200 && res.data && res.data.code === 200) {
          return res.data
        } else {
          console.error('MD5 validation failed', res.data)
          throw new Error('MD5 validation failed')
        }
      })
      .catch(err => {
        console.error(err)
        throw new Error('MD5 validation failed')
      })
  }

  private prepareChunkUploadQueue(
    needUploadChunks: FilePieceUploadStatus[],
    item: UploadItem,
  ): void {
    const totalChunk = Math.max(1, Math.ceil(item.file.size / this.chunkSize))
    item.totalChunksNum = totalChunk
    needUploadChunks.forEach(chunkInfo => {
      const { chunkId, url, headers } = chunkInfo
      const start = chunkId * this.chunkSize
      const end =
        start + this.chunkSize > item.file.size
          ? item.file.size
          : start + this.chunkSize

      const chunkPromiseFn: ChunkPromiseFn = () => {
        // Slice the file here, when the chunk is about to be uploaded
        const binaryData = item.file.slice(start, end)

        const requestOptions: AxiosRequestConfig = {
          url,
          method: 'put',
          headers: { ...headers },
          data: binaryData,
          onUploadProgress: (progressEvent: any) => {
            const progress =
              progressEvent.loaded / progressEvent.total / item.totalChunksNum!
            item.chunksProgressArr[chunkId] = progress
            item.progress = Math.floor(
              item.chunksProgressArr.reduce((a, b) => a + b) * 100,
            )
            if (item.progressNotifyCallback) {
              item.progressNotifyCallback(item.progress)
            }
          },
        }

        return Axios.request(requestOptions) as AxiosPromise<any>
      }

      chunkPromiseFn.isUploading = false
      chunkPromiseFn.chunkId = chunkId

      item.chunkUploadPromiseQueue.push(chunkPromiseFn)
    })
  }

  private prepareChunkUploadQueue2(
    needUploadChunks: FilePieceUploadStatus[],
    item: UploadItem,
  ): void {
    needUploadChunks.forEach(chunkInfo => {
      const { chunkId, url, headers } = chunkInfo
      const start = chunkId * this.chunkSize
      const end =
        start + this.chunkSize > item.file.size
          ? item.file.size
          : start + this.chunkSize
      const binaryData = item.file.slice(start, end)

      const requestOptions: AxiosRequestConfig = {
        url,
        method: 'put',
        headers: { ...headers },
        data: binaryData,
        onUploadProgress: (progressEvent: any) => {
          const progress =
            progressEvent.loaded / progressEvent.total / item.totalChunksNum!
          item.chunksProgressArr[chunkId] = progress
          item.progress = Math.floor(
            item.chunksProgressArr.reduce((a, b) => a + b) * 100,
          )
        },
      }

      const chunkPromiseFn: ChunkPromiseFn = () =>
        Axios.request(requestOptions) as AxiosPromise<any>
      chunkPromiseFn.isUploading = false
      chunkPromiseFn.chunkId = chunkId

      item.chunkUploadPromiseQueue.push(chunkPromiseFn)
    })
  }

  private getUnfinishedChunkArr(
    list: FilePieceUploadStatus[],
    item: UploadItem,
  ): FilePieceUploadStatus[] {
    const totalChunks = Math.max(Math.ceil(item.file.size / this.chunkSize), 1)
    return list.filter((chunkInfo, index) => {
      const isLastChunk = index === totalChunks - 1
      const expectedSize = isLastChunk
        ? item.file.size - (totalChunks - 1) * this.chunkSize
        : this.chunkSize
      return chunkInfo.size !== expectedSize
    })
  }

  private cleanupUploadItem(item: UploadItem): void {
    // Remove references to large objects and arrays
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    item.file = undefined
    item.chunksProgressArr = []
    item.chunkUploadPromiseQueue = []
    item.chunkUploadingPromiseQueue = []
    item.callback = undefined
    item.options = undefined
  }

  // 下载文件 
  downloadFileByGuid(guid: string, progressCallback?: (progressNum: number) => void, failedCallback?: (error: { code: number; errorMsg: string }) => void) {
    return downloadFileByGuid(guid, this.globalToken, progressCallback, failedCallback)
  }
}

const fileUploader = new FileUploader()

const setFileUploadConfig = (config: Partial<typeof uploadConfig> & FileUploader & { supportedFileTypes: string[] }) => {
  fileUploader.globalToken = config.globalToken
  fileUploader.globalUploadDirId = config.globalUploadDirId
  fileUploader.globalUploadAppId = config.globalUploadAppId
  if (config.supportedFileTypes) supportedFileTypes = config.supportedFileTypes
  setUploadConfig(config)
}
export { fileUploader, setFileUploadConfig }
